/*
The MIT License

Copyright (c) 2010 Michael Webb

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 */
//EL
(function() {
	/**
	 * @namespace
	 */
	CA.Workflows = {};
	function ConfigError(name) {
		return new Error(name + " config option(s) is required");
	}
	function CheckConfig(o, required) {
		var errors = [];
		for ( var i = 0; i < required.length; i++) {
			var option = required[i];
			if (!o[option]) {
				errors.push(option);
			}
		}
		if (errors.length > 0) {
			throw ConfigError(errors.join(","));
		}
	}
	var workflows_by_type = {};
	var workflows_by_name = {};
	function RegisterWorkflow(w) {
		if (workflows_by_name[w["name"]]) {
			throw new Error(w["name"] + " is not a unique workflow name.");
		}
		workflows_by_name[w["name"]] = w;
		if (!workflows_by_type[w["type"]]) {
			workflows_by_type[w["type"]] = [];
		}
		workflows_by_type[w["type"]].push(w);
	}
	function UnregisterWorkflow(w) {
		delete workflows_by_name[w["name"]];
		delete workflows_by_type[w["type"]][w["name"]];
	}
	/**
	 * A transition within the workflow. This object is not directly
	 * constructed, instead use AddTransition
	 * 
	 * @class
	 */
	CA.Workflows.TransitionConfig = {	
		/**
		 * (Required) the name of the transition
		 * @type string
		 */
		name : "",
		/**
		 * (Required) the state the content is coming from
		 * @type string
		 */
		from_state : "",
		/**
		 * (Required) the state the content is going to
		 * @type string
		 */
		to_state : "",
		/**
		 * A function that is called with the content object being tested
		 * for this transition and additional info. It returns true if the object
		 * can make this transition, and false if not.
		 * @param {Object} content the object for which valid transitions are being looked up for
		 * @param {Object} info additional information that will be passed on to checks and callbacks
		 */
		check : function(content,info){},
		/**
		 * A function that will be called with the content object, the
		 * related workflow, and an additional info argument when the object makes
		 * this transition.
		 * @param {Object} content the object for which valid transitions are being looked up for
		 * @param {CA.Workflows.Workflow} workflow the workflow
		 * @param {Object} info additional information that will be passed on to checks and callbacks
		 */
		callback : function(content,workflow,info){}
	};
	/**
	 * @class
	 */
	CA.Workflows.StateConfig = {
		/** @lends CA.Workflows.StateConfig.prototype */
		/**
		 * (Required) the name of the state
		 * @type string 
		 */
		name : "",
		/**
		 * (Optional) a function that will be called with the content object, the
		 * related workflow, and an additional info argument when the object
		 * transitions to this state.
		 * @param {Object} content the object for which valid transitions are being looked up for
		 * @param {CA.Workflows.Workflow} workflow the workflow
		 * @param {Object} info additional information that will be passed on to checks and callbacks
		 */
		callback : function(content,workflow,info){}
	};
	/**
	 * @class
	 */
	CA.Workflows.WorkFlowConfig = {	
		/**
		 * (Required) the 'type' of the workflow
		 * @type string 
		 */
		type : "",
		/**
		 * (Required) the name of the workflow
		 * @type string 
		 */
		name : "",	
		/**
		 * (Required) the name of the initial state in the workflow
		 * @type string
		 */
		initial : "",	
		/**
		 * (Required) the name of the attribute that the current statename will be
	 	 * stored in on objects participating in workflow
		 * @type string
		 */
		attribute : "",	
		/**
		 * (Optional) a description of this workflow
		 * @type string
		 */
		description : "",
		/**
		 * (Optional) an array of interfaces for content that this workflow supports
		 * @type array
		 */
		content_types : []
	};
	/**
	 * A function that has no
	 * 
	 * @class Workflow
	 * @constructor
	 * @param {CA.Workflows.WorkFlowConfig} o the config object
	 * @param {boolean} register Indicates if the workflow should be registered during construction
	 */
	CA.Workflows.Workflow = function(config, register) {
		CA.apply(config, this, true);
		CheckConfig(this, [ "type", "name", "initial", "attribute" ]);
		this["config"] = config;
		var states = this["states"] || [];
		var transitions = this["transitions"] || [];
		this["states"] = {};
		this["transitions"] = {};
		this["transition_lookup"] = {};
		for ( var i = 0; i < states.length; i++) {
			this["AddState"](states[i]);
		}
		for ( var j = 0; j < transitions.length; j++) {
			this["AddTransition"](transitions[j]);
		}
		if (register) {
			this["Register"]();
		}
	};
	/**
	 * The workflow's description
	 *  @name CA.Workflows.Workflow.prototype.description
	 *  @type string
	 */
	CA.Workflows.Workflow.prototype["description"] = "";
	/**
	 * List of Interfaces that can participate in this workflow
	 *  @name CA.Workflows.Workflow.prototype.content_types
	 *  @type array
	 */
	CA.Workflows.Workflow.prototype["content_types"] = [];
	/**
	 * Registers the workflow for use
	 * @name CA.Workflows.Workflow.prototype.Register
	 * @function
	 */
	CA.Workflows.Workflow.prototype["Register"] = function() {
		RegisterWorkflow(this);
	};
	/**
	 * Unregisters the workflow
	 * @name CA.Workflows.Workflow.prototype.Unregister
	 * @function
	 */
	CA.Workflows.Workflow.prototype["Unregister"] = function() {
		UnregisterWorkflow(this);
	};
	/**
	 * Adds a new state to the workflow
	 * @name CA.Workflows.Workflow.prototype.AddState
	 * @function
	 * @param {CA.Workflows.StateConfig} config A config object
	 */
	CA.Workflows.Workflow.prototype["AddState"] = function(config) {
		CheckConfig(config, [ "name" ]);
		if (this["states"][config["name"]]) {
			throw new Error("This state name/alias is taken for this workflow.");
		}
		this["states"][config["name"]] = config;
	};
	/**
	 * Adds a new transition to the workflow
	 * @name CA.Workflows.Workflow.prototype.AddTransition
	 * @function
	 * @param {CA.Workflows.TransitionConfig} config config object
	 */
	CA.Workflows.Workflow.prototype["AddTransition"] = function(config) {
		CheckConfig(config, [ "from_state", "to_state", "name" ]);
		if (this["transitions"][config["name"]]) {
			throw new Error("This transition name is taken for this workflow.");
		}
		if (!this["states"][config["from_state"]]) {
			throw new Error(
					"The 'from_state' for this transition does not exist.");
		}
		if (!this["states"][config["to_state"]]) {
			throw new Error(
					"The 'to_state' for this transition does not exist.");
		}
		var froms = CA.NameSpace(config["from_state"], this["transition_lookup"]);
		if (!froms[config["to_state"]]) {
			froms[config["to_state"]] = [];
		}
		froms[config["to_state"]].push(config);
		this["transitions"][config["name"]] = config;
	};
	/**
	 * Tests to see if the workflow accepts this Object/Interface
	 * @name CA.Workflows.Workflow.prototype.Accepts
	 * @function
	 * @param {Object/String} obj If it is an object, the object is tested to see if it
	 *            meets any of the interfaces in the workflows content_types. If
	 *            content_types is empty, this function returns true. If obj is
	 *            a string, the function checks to see if it is one of the
	 *            strings in content_types.
	 */
	CA.Workflows.Workflow.prototype["Accepts"] = function(obj) {
		if (obj !== null && this["content_types"].length) {
			if (typeof obj === "string" && this["content_types"].indexOf(obj) != -1) {
				return true;
			} else {
				if (CA.Interfaces) {
					for ( var i = 0; i < this["content_types"].length; i++) {
						if (CA.Interfaces.Check(obj,
								this["content_types"][i])) {
							return true;
						}
					}
				} else {
					throw new Error(
							"Cannot verify interface on object as CA.Interfaces is not included.");
				}
			}
			return false;
		}
		return true;
	};
	/**
	 * Gets a list of valid transitions
	 * @name CA.Workflows.Workflow.prototype.GetTransitions
	 * @function
	 * @param {Object} content the object for which valid transitions are being looked up for
	 * @param {Object} info additional information that will be passed on to checks and callbacks
	 * @return {Array} a list of valid transitions
	 */
	CA.Workflows.Workflow.prototype["GetTransitions"] = function(content, info) {
		var from_state = this["GetStateOf"](content);
		var from_ = from_state["name"];
		var froms = CA.NameSpace(from_, this["transition_lookup"]);
		var trans = [];
		for ( var attr in froms) {
			if (froms.hasOwnProperty(attr)) {
				var tos = froms[attr];
				for ( var name in tos) {
					if (tos.hasOwnProperty(name)) {
						var transition = tos[name];
						if (content === null || (transition["check"] && transition["check"](content, info))) {
							trans.push( {
								"transition" : transition,
								"state" : this["states"][attr]
							});
						}
					}
				}
			}
		}
		return trans;
	};
	/**
	 * Initializes the object to the initial state
	 * @name CA.Workflows.Workflow.prototype.Initialize
	 * @function
	 * @param {Object} content the object for which valid transitions are being looked up for
	 * @param {Object} info additional information that will be passed on to checks and callbacks
	 * @return {Object} the initial state
	 */
	CA.Workflows.Workflow.prototype["Initialize"] = function(content, info) {
		var state = this["states"][this["initial"]];
		if (!state) {
			throw new Error("Unknown state:" + this["initial"]);
		}
		content[this["attribute"]] = this["initial"];
		if (state["callback"]) {
			state["callback"](content, this, info);
		}
		return state;
	};
	/**
	 * Gets the state of the content. If the content has not been initialized it
	 * will be initialized to the initial state.
	 * @name CA.Workflows.Workflow.prototype.GetStateOf
	 * @function
	 * @param {Object} content the object for which valid transitions are being looked up for
	 * @param {Object} info additional information that will be passed on to checks and callbacks
	 * @return {Object} the content's state
	 */
	CA.Workflows.Workflow.prototype["GetStateOf"] = function(content, info) {
		var state_name = content[this["attribute"]];
		if (!state_name) {
			return this["Initialize"](content, info);
		}
		var state = this["states"][state_name];
		if (!state) {
			throw new Error("Unknown state:" + state_name);
		}
		return state;
	};
	/**
	 * Resets the content on its current state by calling the states callback
	 * again. If the content has not been initialized it will be initialized to
	 * the initial state.
	 * @name CA.Workflows.Workflow.prototype.Reset
	 * @function
	 * @param {Object} content the object for which valid transitions are being looked up for
	 * @param {Object} info additional information that will be passed on to checks and callbacks
	 * @return {Object} the content's state
	 */
	CA.Workflows.Workflow.prototype["Reset"] = function(content, info) {
		var state_name = content[this["attribute"]] || this["initial"];
		var state = this["states"][state_name];
		if (!state) {
			throw new Error("Unknown state:" + state_name);
		}
		content[this["attribute"]] = state_name;
		if (state["callback"]) {
			state["callback"](content, this, info);
		}
		return state;
	};
	/**
	 * Gets a state by name
	 * @name CA.Workflows.Workflow.prototype.GetState
	 * @function
	 * @param {string} name The name of the state
	 * @return {Object} the state matching name
	 */
	CA.Workflows.Workflow.prototype["GetState"] = function(name) {
		return this["states"][name] || null;
	};
	/**
	 * Gets a transition by name
	 * @name CA.Workflows.Workflow.prototype.GetTransition
	 * @function
	 * @param {string} name The name of the transition
	 * @return {Object} the transition matching name
	 */
	CA.Workflows.Workflow.prototype["GetTransition"] = function(name) {
		return this["transitions"][name] || null;
	};
	/**
	 * Gets a transition by name
	 * @name CA.Workflows.Workflow.prototype.Transition
	 * @function
	 * @param {Object} content the object to be transitioned
	 * @param {string} name The name of the state to be transfered to, or the transition to transfer with
	 * @param {Object} info additional information that will be passed on to checks and callbacks
	 * @return {boolean} success
	 */
	CA.Workflows.Workflow.prototype["Transition"] = function(content, to_thing, info) {
		var from_ = this["GetStateOf"](content);
		var transition = this["transitions"][to_thing];
		if (!transition) {
			var to_ = this["states"][to_thing];
			if (!to_) {
				throw new Error("Unknown state:" + to_thing);
			}
			try {
				var transList = this["transition_lookup"][from_["name"]][to_["name"]].slice(0);
				for ( var i = 0; i < transList.length; i++) {
					var t = transList[i];
					if (!t["check"] || t["check"](content, info)) {
						transition = t;
						break;
					}
				}
			} catch (e) {
			}
		} else {
			if (transition["check"] && !transition["check"](content, info)) {
				transition = null;
			}
		}
		if (!transition) {
			return false;
		} else {
			if (transition["callback"]) {
				transition["callback"](content, this, info);
			}
			var state = this["states"][transition["to_state"]];
			if (state["callback"]) {
				state["callback"](content, this, info);
			}
			content[this["attribute"]] = state["name"];
			return true;
		}
	};
	var WorkFlow = CA.Workflows.Workflow;
	/**
	 * Gets a workflow by name
	 * @function GetWorkflow
	 * @param {string} name The name of the workflow
	 * @return {Workflow} the workflow matching name
	 */
	CA.Workflows.GetWorkflow = function(name) {
		return workflows_by_name[name] || null;
	};
	/**
	 * Gets a workflow by name
	 * @function FindWorkflow
	 * @param {Object/String} obj An object or string (interface name) to be used in the workflow
	 * @param {string} wType The type of the workflow
	 * @return {Workflow} the matching workflow
	 */
	CA.Workflows.FindWorkflow = function(o, wType) {
		var wfList = workflows_by_type[wType];
		if (wfList) {
			for ( var i = 0; i < wfList.length; i++) {
				var workflow = wfList[i];
				if (workflow["Accepts"](o)) {
					return workflow;
				}
			}
		}
		return null;
	};
})();
